Skip to content

Conversation

@kaumudpa
Copy link

@kaumudpa kaumudpa commented Dec 16, 2025

What kind of change does this PR introduce?

Feature: Add AWS S3 and MinIO (S3-compatible) storage provider support

This PR introduces a new storage provider option (s3) that allows users to store media files on AWS S3 or any S3-compatible service like MinIO, DigitalOcean Spaces, or Backblaze B2.

Why was this change needed?

Fixes #1124
Fixes #1119
Fixes #996

Currently, Postiz only supports two storage options:

  • Local storage - Files stored on the server filesystem
  • Cloudflare R2 - Cloud object storage via Cloudflare

Many self-hosted users have requested S3 support because:

  1. Existing Infrastructure: Users already have AWS S3 buckets or MinIO deployments and don't want to create a separate Cloudflare account just for Postiz.

  2. Cost Optimization: AWS S3 may be more cost-effective for users already in the AWS ecosystem, and MinIO provides completely self-hosted object storage with zero external dependencies.

  3. Data Sovereignty: Self-hosted MinIO allows users to keep all media on their own infrastructure - critical for GDPR compliance, enterprise security requirements, and air-gapped environments.

  4. Vendor Neutrality: Adding S3 support gives users more flexibility in choosing their storage provider.

Changes Made

New Files

File Purpose
libraries/nestjs-libraries/src/upload/s3.storage.ts S3 storage provider implementing IUploadProvider
libraries/nestjs-libraries/src/upload/s3.uploader.ts Multipart upload handler for S3/MinIO
libraries/nestjs-libraries/src/upload/s3.utils.ts Helper functions for S3 URL construction

Modified Files

File Change
upload.factory.ts Added 's3' case to factory pattern
media.controller.ts Route uploads to S3 or R2 based on STORAGE_PROVIDER
media.service.ts Added storage.removeFile() call when deleting media
uppy.upload.ts Added 's3' case (uses same AwsS3Multipart plugin)
variable.context.tsx Added 's3' to storage provider type
cloudflare.storage.ts Enabled removeFile() (was no-op), fixed URL trailing slash
r2.uploader.ts Fixed URL construction for reliability
local.storage.ts Implemented actual file deletion with logging
.env.example Added S3 configuration variables

New Environment Variables

STORAGE_PROVIDER="s3"
S3_ENDPOINT=""              # Optional: Custom endpoint for MinIO
S3_ACCESS_KEY="your-key"
S3_SECRET_KEY="your-secret"
S3_BUCKET="your-bucket"
S3_REGION="us-east-1"
S3_BUCKET_URL=""            # Optional: Custom public URL

Backward Compatibility

This change is 100% backward compatible:

  • All S3 code paths are guarded by STORAGE_PROVIDER === 's3'
  • Existing local and cloudflare providers work exactly as before
  • No database migrations required
  • Changes to R2/Cloudflare code are bug fixes only (enabling delete, fixing URL handling)

Other information:

Configuration Examples

AWS S3:

STORAGE_PROVIDER="s3"
S3_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
S3_SECRET_KEY="wJalrXUtnFEMI/..."
S3_BUCKET="my-postiz-bucket"
S3_REGION="us-west-2"
S3_BUCKET_URL="https://my-postiz-bucket.s3.us-west-2.amazonaws.com"

MinIO (self-hosted):

STORAGE_PROVIDER="s3"
S3_ENDPOINT="https://minio.example.com"
S3_ACCESS_KEY="minioadmin"
S3_SECRET_KEY="minioadmin"
S3_BUCKET="postiz"
S3_REGION="us-east-1"
S3_BUCKET_URL="https://minio.example.com/postiz"

MinIO Bucket Configuration

MinIO users need to enable public read access on their bucket:

mc anonymous set download myminio/bucket-name

This can be automated in docker-compose with an init container.

Testing Performed

  • ✅ Local storage upload/delete
  • ✅ AWS S3 upload/delete
  • ✅ MinIO upload/delete
  • ✅ Multipart upload for large videos (up to 1GB)
  • ✅ Cloudflare R2 regression test
  • ✅ Date-based directory structure (YYYY/MM/DD/filename)

Future Considerations

  • Documentation page for S3/MinIO setup (similar to existing R2 docs)
  • Potential support for other S3-compatible services (Backblaze B2, DigitalOcean Spaces)

Checklist:

Put a "X" in the boxes below to indicate you have followed the checklist;

  • I have read the CONTRIBUTING guide.
  • I checked that there were not similar issues or PRs already open for this.
  • This PR fixes just ONE issue (do not include multiple issues or types of change in the same PR) For example, don't try and fix a UI issue and include new dependencies in the same PR.

@vercel
Copy link

vercel bot commented Dec 16, 2025

@kaumudpa is attempting to deploy a commit to the Listinai Team on Vercel.

A member of the Team first needs to authorize it.

@kaumudpa kaumudpa marked this pull request as draft December 16, 2025 15:09
@kaumudpa kaumudpa marked this pull request as ready for review December 16, 2025 15:09
@kaumudpa kaumudpa marked this pull request as draft December 16, 2025 15:12
@kaumudpa kaumudpa marked this pull request as draft December 16, 2025 15:41
Add native support for AWS S3 and S3-compatible storage services (MinIO,
DigitalOcean Spaces, Backblaze B2) as an alternative to existing local
and Cloudflare R2 storage options.

New features:
- Generic 's3' storage provider using AWS SDK v3
- Support for custom endpoints (MinIO/self-hosted S3-compatible services)
- Multipart upload support for large files (videos up to 1GB)
- Date-based directory organization (YYYY/MM/DD/filename)
- Automatic file deletion from storage when media is deleted

Bug fixes included:
- Enable actual file deletion from Cloudflare R2 (was no-op)
- Add file deletion from local storage
- Fix URL trailing slash handling for all providers
- Add null checks and error handling for edge cases
- Add fallback for unrecognized MIME types

New environment variables:
- S3_ENDPOINT (optional, for MinIO)
- S3_ACCESS_KEY
- S3_SECRET_KEY
- S3_BUCKET
- S3_REGION
- S3_BUCKET_URL (optional)

Fully backward compatible - existing local and cloudflare providers unchanged.
@kaumudpa kaumudpa marked this pull request as ready for review December 16, 2025 15:46
@kaumudpa
Copy link
Author

Closes #1125

@kaumudpa
Copy link
Author

@nevo-david Can we expect the PR's we create to get merged or atleast any feedback if you do not intend to merge.

@Fer-r
Copy link
Contributor

Fer-r commented Dec 26, 2025

+1 - This would be great for self-hosted users behind Cloudflare tunnels who need to bypass the 100MB upload limit via multipart uploads to local MinIO!

@diffray-bot
Copy link

Changes Summary

This PR introduces AWS S3 and MinIO (S3-compatible) storage provider support as a new media storage option alongside existing local and Cloudflare R2 providers. It adds three new S3-specific modules, integrates S3 into the upload factory pattern, and enables file deletion functionality across all storage providers.

Type: feature

Components Affected: Storage abstraction layer, Media upload/download pipeline, File deletion functionality, Backend API routes, Frontend upload component, Environment configuration

Files Changed
File Summary Change Impact
...aries/nestjs-libraries/src/upload/s3.storage.ts New S3 storage provider implementing IUploadProvider interface with support for AWS S3 and MinIO endpoints, including upload and delete operations. 🔴
...ries/nestjs-libraries/src/upload/s3.uploader.ts New multipart upload handler for S3/MinIO with support for large file uploads via presigned URLs and complete upload coordination. 🔴
...braries/nestjs-libraries/src/upload/s3.utils.ts Helper functions for S3 URL construction supporting AWS virtual-hosted style, MinIO path-style, and custom bucket URLs. 🟡
...s/nestjs-libraries/src/upload/upload.factory.ts Added 's3' case to factory pattern to instantiate S3Storage with environment variables and validation. ✏️ 🟡
...apps/backend/src/api/routes/media.controller.ts Added S3 uploader initialization and conditional routing of multipart uploads to S3 or R2 based on STORAGE_PROVIDER setting. ✏️ 🟡
...ries/src/database/prisma/media/media.service.ts Added deleteMedia method that calls storage.removeFile() to delete files from storage when media records are deleted. ✏️ 🟡
...stjs-libraries/src/upload/cloudflare.storage.ts Implemented removeFile() method (was previously a no-op) and fixed URL trailing slash normalization. ✏️ 🟡
...es/nestjs-libraries/src/upload/local.storage.ts Implemented actual file deletion via fs.unlink with comprehensive error handling and fallback logic for invalid URLs. ✏️ 🟡
...ries/nestjs-libraries/src/upload/r2.uploader.ts Fixed URL construction in completeMultipartUpload to use reliable key from request instead of fragile Location header parsing. ✏️ 🟢
...act-shared-libraries/src/helpers/uppy.upload.ts Added 's3' case to getUppyUploadPlugin factory to use AwsS3Multipart plugin with same configuration as Cloudflare. ✏️ 🟡
/tmp/workspace/.env.example Added S3/MinIO environment variables documentation with optional endpoint for MinIO support. ✏️ 🟢
...orkspace/apps/frontend/src/app/(app)/layout.tsx Minor change to frontend layout (impact unclear from diff stat). ✏️ 🟢
...ce/apps/frontend/src/app/(extension)/layout.tsx Minor change to extension layout (impact unclear from diff stat). ✏️ 🟢
.../frontend/src/components/media/new.uploader.tsx Minor updates to media uploader component (impact unclear from diff stat). ✏️ 🟢
...e/prisma/integrations/integration.repository.ts Minor changes to integration repository (impact unclear from diff stat). ✏️ 🟢
...ared-libraries/src/helpers/variable.context.tsx Added 's3' to storage provider type definition. ✏️ 🟢
Architecture Impact
  • New Patterns: Factory pattern extended for new storage provider, Provider-specific configuration via environment variables, URL format abstraction for multi-backend compatibility
  • Dependencies: added: @aws-sdk/client-s3 (AWS SDK for S3 operations), added: @aws-sdk/s3-request-presigner (for presigned URLs)
  • Coupling: Minimal coupling introduced. S3-specific code is isolated in dedicated modules and guarded by STORAGE_PROVIDER === 's3' conditionals. Factory pattern maintains separation of concerns between providers.
  • Breaking Changes: None - fully backward compatible. All S3 code paths are conditional based on STORAGE_PROVIDER environment variable.

Risk Areas: URL parsing and key extraction logic in s3.storage.ts removeFile() method - handles multiple URL formats (path-style, virtual-hosted, custom CDN) with fallback parsing, Multipart upload completion - Location header is replaced with constructed URL, depends on reliable key tracking, File deletion error handling - storage.removeFile() failures in media.service.ts are caught but logged; soft delete continues (could lead to orphaned files), Environment variable validation - S3 requires 4 mandatory variables; missing variables throw during factory instantiation, Date-based directory structure - generateDatePath() is duplicated across S3Storage and S3Uploader classes (DRY violation), Credentials in environment - S3_ACCESS_KEY and S3_SECRET_KEY are exposed as environment variables (standard but worth noting)

Suggestions
  • Extract date path generation into a shared utility function used by both S3Storage and S3Uploader to reduce code duplication
  • Consider logging file deletion failures with more context (file path, org ID) for troubleshooting orphaned files
  • Add integration tests for S3 removeFile() with different URL formats (path-style, virtual-hosted, custom CDN)
  • Document the ACL: 'public-read' requirement for bucket permissions in setup guide
  • Consider retry logic for multipart upload operations given network transience
  • The s3.utils.ts functions like isStorageUrl() and getCurrentBucketUrl() could be used more widely in other components for consistency

Full review in progress... | Powered by diffray

Comment on lines 161 to 187
@Res() res: Response,
@Param('endpoint') endpoint: string
) {
const upload = await handleR2Upload(endpoint, req, res);
// Route to appropriate handler based on storage provider
let upload;
if (process.env.STORAGE_PROVIDER === 's3' && this.s3Uploader) {
upload = await this.s3Uploader.handleUpload(endpoint, req, res);
} else {
upload = await handleR2Upload(endpoint, req, res);
}

if (endpoint !== 'complete-multipart-upload') {
return upload;
}

// Check if response was already sent (e.g., error handler sent it)
if (res.headersSent) {
return;
}

// @ts-ignore - upload is CompleteMultipartUploadCommandOutput here
if (!upload?.Location) {
return res.status(500).json({ error: 'Upload failed - no location returned' });
}
// @ts-ignore
const name = upload.Location.split('/').pop();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Missing idempotency for multipart upload completion
Agent: bugs

Category: bug

Description:
The uploadFile endpoint saves media after multipart upload completes but lacks idempotency protection. Network failures after upload but before DB save could cause duplicate entries on retry.

Suggestion:
Implement idempotent file saving using uploadId as idempotency key, or add unique constraint on file URL/checksum.

Confidence: 70%
Rule: gen_double_charge_no_optimistic_lock
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +71 to +85
async uploadSimple(path: string): Promise<string> {
const loadImage = await fetch(path);
const contentType =
loadImage?.headers?.get('content-type') ||
loadImage?.headers?.get('Content-Type');
// Fallback to 'bin' if MIME type is unrecognized, or try to extract from URL
const extension = getExtension(contentType) || path.split('.').pop()?.split('?')[0] || 'bin';
const id = makeId(10);
const datePath = this.getDatePath();
const key = `${datePath}/${id}.${extension}`;

const params = {
Bucket: this._bucketName,
Key: key,
Body: Buffer.from(await loadImage.arrayBuffer()),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Unhandled HTTP error status in uploadSimple()
Agent: bugs

Category: bug

Description:
fetch() does not throw on HTTP error statuses (4xx/5xx). The code proceeds to call arrayBuffer() without checking response.ok, potentially processing error responses as valid data.

Suggestion:
Check response.ok before processing: if (!loadImage.ok) throw new Error(HTTP ${loadImage.status}: ${loadImage.statusText});

Confidence: 95%
Rule: bug_missing_try_catch
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +104 to +114
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: data,
ContentType: contentType,
ACL: 'public-read',
});

await this.client.send(command);
return this.bucketUrl + '/' + key;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - S3 uploads lack explicit server-side encryption
Agent: compliance

Category: security

Description:
PutObjectCommand does not include ServerSideEncryption field. Files rely on bucket-level default encryption settings if any. Explicit encryption ensures compliance regardless of bucket config.

Suggestion:
Add ServerSideEncryption: 'AES256' or 'aws:kms' to PutObjectCommand parameters.

Confidence: 75%
Rule: soc2_encrypt_data_at_rest
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +259 to +276
export function createS3Uploader(): S3Uploader {
const required = ['S3_ACCESS_KEY', 'S3_SECRET_KEY', 'S3_REGION', 'S3_BUCKET'];
const missing = required.filter((v) => !process.env[v]);
if (missing.length > 0) {
throw new Error(
`Missing required environment variables for S3 uploader: ${missing.join(', ')}`
);
}

return new S3Uploader({
endpoint: process.env.S3_ENDPOINT || undefined,
accessKey: process.env.S3_ACCESS_KEY!,
secretKey: process.env.S3_SECRET_KEY!,
region: process.env.S3_REGION!,
bucketName: process.env.S3_BUCKET!,
bucketUrl: process.env.S3_BUCKET_URL || undefined,
});
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - S3 credentials from environment variables
Agent: compliance

Category: security

Description:
createS3Uploader() loads AWS credentials from process.env. This is a common pattern but lacks secrets manager integration for credential rotation.

Suggestion:
Consider AWS Secrets Manager integration if compliance requirements mandate credential rotation.

Confidence: 60%
Rule: soc2_secrets_manager_rotation
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 50 to 51
@Delete('/:id')
deleteMedia(@GetOrgFromRequest() org: Organization, @Param('id') id: string) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - No audit logging for media deletion
Agent: compliance

Category: security

Description:
The deleteMedia endpoint directly calls _mediaService.deleteMedia() without audit logging. No record of who deleted what media or when.

Suggestion:
Add audit logging before deletion: log actor (user/org ID), action, resource ID, timestamp.

Confidence: 80%
Rule: compliance_audit_trail
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +109 to +131
ACL: 'public-read',
});

await this.client.send(command);
return this.bucketUrl + '/' + key;
}

private async createMultipartUpload(req: Request, res: Response) {
const { file, fileHash, contentType } = req.body;
if (!file?.name) {
return res.status(400).json({ error: 'Missing file name' });
}
const fileExtension = path.extname(file.name);
const randomFilename = this.generateRandomString() + fileExtension;
const datePath = this.getDatePath();
const key = `${datePath}/${randomFilename}`;

try {
const command = new CreateMultipartUploadCommand({
Bucket: this.bucketName,
Key: key,
ContentType: contentType,
ACL: 'public-read',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Public read ACL hardcoded for all S3 uploads
Agent: security

Category: security

Description:
All S3 uploads use hardcoded ACL: 'public-read' (lines 109 and 131). Every uploaded file is publicly accessible without authentication, which may not be appropriate for all content types.

Suggestion:
Make ACL configurable based on file type or use case. Consider 'private' as default with signed URLs for access.

Confidence: 75%
Rule: sec_access_control
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +87 to +105
ACL: 'public-read' as const,
};

const command = new PutObjectCommand(params);
await this._client.send(command);

return `${this._bucketUrl}/${key}`;
}

async uploadFile(file: Express.Multer.File): Promise<any> {
try {
const id = makeId(10);
const extension = mime.extension(file.mimetype) || '';
const datePath = this.getDatePath();
const key = `${datePath}/${id}.${extension}`;

const command = new PutObjectCommand({
Bucket: this._bucketName,
ACL: 'public-read',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Public ACL hardcoded for all S3 storage uploads
Agent: security

Category: security

Description:
S3Storage uses hardcoded ACL: 'public-read' in both uploadSimple (line 87) and uploadFile (line 105). All files are world-readable.

Suggestion:
Make ACL configurable. Use 'private' ACL as default and implement signed URLs for controlled access.

Confidence: 75%
Rule: sec_access_control
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +181 to 187
// @ts-ignore - upload is CompleteMultipartUploadCommandOutput here
if (!upload?.Location) {
return res.status(500).json({ error: 'Upload failed - no location returned' });
}
// @ts-ignore
const name = upload.Location.split('/').pop();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Unsafe type casting for response object
Agent: react

Category: quality

Description:
Multiple @ts-ignore comments (lines 181, 185, 191) bypass TypeScript checks when accessing upload.Location. This indicates improper typing of the CompleteMultipartUploadCommandOutput response.

Suggestion:
Define proper types for the upload response and use type guards: if (upload && 'Location' in upload) { ... }

Confidence: 80%
Rule: ts_classify_http_errors_with_type_safe_narr
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +131 to +159
async removeFile(filePath: string): Promise<void> {
// Extract the key from the full URL
// URL formats:
// - Path-style (MinIO): https://minio.example.com/bucket/2025/12/16/abc123.png
// - Virtual-hosted (AWS): https://bucket.s3.region.amazonaws.com/2025/12/16/abc123.png
// - Custom bucket URL: https://cdn.example.com/2025/12/16/abc123.png
let key = '';

// Primary: Check if URL starts with configured bucket URL
if (filePath.startsWith(this._bucketUrl)) {
key = filePath.substring(this._bucketUrl.length).replace(/^\//, '');
} else {
// Fallback: Try to parse URL and extract path after bucket name
try {
const url = new URL(filePath);
const pathParts = url.pathname.split('/').filter(Boolean);

// Check if first part is the bucket name (path-style URLs)
if (pathParts[0] === this._bucketName) {
key = pathParts.slice(1).join('/');
} else {
// Virtual-hosted style or custom URL - entire path is the key
key = pathParts.join('/');
}
} catch {
// Not a valid URL, use as-is (might be just a key)
key = filePath;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - File deletion without ownership validation
Agent: security

Category: security

Description:
The removeFile method accepts URLs and extracts S3 keys. While it validates the URL structure (lines 140-158), there's no ownership check to verify the file belongs to the requesting user/organization.

Suggestion:
Implement ownership checks at the service layer to verify file path matches expected org/user prefixes before deletion.

Confidence: 70%
Rule: sec_access_control
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 161 to 187
@Res() res: Response,
@Param('endpoint') endpoint: string
) {
const upload = await handleR2Upload(endpoint, req, res);
// Route to appropriate handler based on storage provider
let upload;
if (process.env.STORAGE_PROVIDER === 's3' && this.s3Uploader) {
upload = await this.s3Uploader.handleUpload(endpoint, req, res);
} else {
upload = await handleR2Upload(endpoint, req, res);
}

if (endpoint !== 'complete-multipart-upload') {
return upload;
}

// Check if response was already sent (e.g., error handler sent it)
if (res.headersSent) {
return;
}

// @ts-ignore - upload is CompleteMultipartUploadCommandOutput here
if (!upload?.Location) {
return res.status(500).json({ error: 'Upload failed - no location returned' });
}
// @ts-ignore
const name = upload.Location.split('/').pop();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Missing validation in multipart upload context
Agent: security

Category: security

Description:
The uploadFile endpoint routes multipart upload requests (line 157-170) based on endpoint parameter. While org is retrieved via @GetOrgFromRequest(), there's no validation that the uploadId/key belongs to the authenticated organization.

Suggestion:
Validate that multipart upload context (uploadId, key) belongs to the authenticated organization before processing.

Confidence: 70%
Rule: sec_broken_access_control
Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@diffray-bot
Copy link

Review Summary

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Validated 95 issues: 63 kept, 32 filtered

Issues Found: 63

💬 See 44 individual line comment(s) for details.

📊 34 unique issue type(s) across 63 location(s)

📋 Full issue list (click to expand)

🔴 CRITICAL - Sensitive error objects exposed in logging

Agent: security

Category: security

Why this matters: AWS SDK errors can contain request IDs, account info, and operational details that could aid attackers.

File: libraries/nestjs-libraries/src/upload/s3.uploader.ts:143-144

Description: Full AWS SDK error objects logged without sanitization at multiple lines (143, 170, 190, 211, 228, 252). These errors may contain sensitive request details, credentials, or internal operational information.

Suggestion: Implement error sanitization. Log only error.code and error.message. Never log full error objects in production: console.log('S3 error:', { code: err.code, message: err.message })

Confidence: 85%

Rule: sec_no_pii_logging


🔴 CRITICAL - Unhandled promise rejection in fetch().json()

Agent: bugs

Category: bug

Why this matters: Unhandled rejections in Promise.all will fail silently or crash the operation without proper user feedback.

File: apps/frontend/src/components/media/new.uploader.tsx:224-230

Description: fetch() and .json() calls inside Promise.all have no error handling. Network failures or invalid JSON will cause unhandled promise rejection.

Suggestion: Wrap in try-catch: try { const res = await fetch(...); if (!res.ok) throw new Error(...); return res.json(); } catch (error) { ... }

Confidence: 85%

Rule: bug_unhandled_promise


🟠 HIGH - Hardcoded dependencies in controller via factory (3 occurrences)

Agent: architecture

Category: quality

Why this matters: Hardcoded dependencies make unit testing difficult and create tight coupling.

📍 View all locations
File Description Suggestion Confidence
apps/backend/src/api/routes/media.controller.ts:37-48 MediaController instantiates storage and s3Uploader at property/constructor level using factories in... Inject storage and s3Uploader as constructor dependencies. Use NestJS providers to configure the app... 85%
libraries/nestjs-libraries/src/database/prisma/media/media.service.ts:25 MediaService instantiates storage directly using UploadFactory.createStorage() at the property level... Inject the IUploadProvider instance as a constructor dependency. Use NestJS DI to provide the approp... 85%
libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:12 IntegrationRepository instantiates storage using UploadFactory.createStorage() at property level, cr... Inject the IUploadProvider instance as a constructor dependency. 85%

Rule: arch_hardcoded_dependency_js


🟠 HIGH - Business logic in controller instead of service (2 occurrences)

Agent: architecture

Category: quality

Why this matters: Controllers should be thin; business logic belongs in services for testability.

📍 View all locations
File Description Suggestion Confidence
apps/backend/src/api/routes/media.controller.ts:51 The MediaController contains significant business logic: subscription checking (line 71-74), image g... Move generateImage and generateImageFromText orchestration to MediaService. Controller should only h... 75%
apps/backend/src/api/routes/media.controller.ts:165-174 The uploadFile method contains conditional logic for routing between S3 and R2 storage providers bas... Encapsulate provider selection in a service or use a strategy pattern. Controller should call a unif... 70%

Rule: nestjs_service_in_controller


🟠 HIGH - Missing lang attribute on html element (3 occurrences)

Agent: accessibility

Category: accessibility

Why this matters: Screen readers rely on lang attribute to use correct pronunciation rules.

📍 View all locations
File Description Suggestion Confidence
apps/frontend/src/app/(app)/layout.tsx:47 The root element is missing a 'lang' attribute. WCAG 3.1.1 requires the default language to b... Add the lang attribute: or dynamically set based on headers. 95%
apps/frontend/src/app/(extension)/layout.tsx:25 The root element is missing a 'lang' attribute. WCAG 3.1.1 requires the default language to b... Add the lang attribute: 95%
apps/frontend/src/components/media/new.uploader.tsx:278-292 The Uppy Dashboard component wrapper div has class 'pointer-events-none' but lacks ARIA labels for s... Add aria-label to the wrapping div: <div className='pointer-events-none bigWrap' aria-label='File up... 65%

Rule: fe_aria_live_regions


🟠 HIGH - Missing idempotency for multipart upload completion (3 occurrences)

Agent: bugs

Category: bug

📍 View all locations
File Description Suggestion Confidence
apps/backend/src/api/routes/media.controller.ts:161-187 The uploadFile endpoint saves media after multipart upload completes but lacks idempotency protectio... Implement idempotent file saving using uploadId as idempotency key, or add unique constraint on file... 70%
libraries/react-shared-libraries/src/helpers/variable.context.tsx:57-62 The useEffect has an empty dependency array [] but uses otherProps. If props change after mount, win... Add otherProps to the dependency array: useEffect(() => { ... }, [otherProps]) or use individual pro... 85%
libraries/nestjs-libraries/src/upload/r2.uploader.ts:199-217 The signPart function parses partNumber using parseInt() without checking for NaN. Unlike s3.uploade... Add validation: if (isNaN(partNumber)) { return res.status(400).json({ error: 'Invalid part number' ... 80%

Rule: gen_double_charge_no_optimistic_lock


🟠 HIGH - Module-level S3 client reads env vars at import time

Agent: architecture

Category: quality

File: libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31

Description: The R2 client is instantiated at module level, reading process.env variables during import. Missing env vars cause module load failures. Also makes testing difficult.

Suggestion: Move client creation into a factory function or service class with lazy initialization and dependency injection support.

Confidence: 85%

Rule: arch_singleton_state_js


🟠 HIGH - Unhandled HTTP error status in uploadSimple()

Agent: bugs

Category: bug

File: libraries/nestjs-libraries/src/upload/s3.storage.ts:71-85

Description: fetch() does not throw on HTTP error statuses (4xx/5xx). The code proceeds to call arrayBuffer() without checking response.ok, potentially processing error responses as valid data.

Suggestion: Check response.ok before processing: if (!loadImage.ok) throw new Error(HTTP ${loadImage.status}: ${loadImage.statusText});

Confidence: 95%

Rule: bug_missing_try_catch


🟠 HIGH - Blocking mkdirSync in async function

Agent: performance

Category: performance

File: libraries/nestjs-libraries/src/upload/local.storage.ts:20

Description: mkdirSync blocks the event loop during async uploadSimple/uploadFile operations. With concurrent uploads, this degrades server performance.

Suggestion: Replace with await mkdir(dir, { recursive: true }) from fs/promises.

Confidence: 90%

Rule: node_blocking_sync_operations


🟠 HIGH - Sequential presigned URL generation in loop (2 occurrences)

Agent: performance

Category: performance

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/s3.uploader.ts:156-173 prepareUploadParts generates signed URLs sequentially with await inside for loop. For multipart uplo... Use Promise.all: const urls = await Promise.all(parts.map(part => getSignedUrl(...))); 90%
libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:529-538 disableIntegrations updates channels one-by-one in a loop. For N channels, this executes N individua... Use updateMany: await this._integration.model.integration.updateMany({ where: { id: { in: getChannel... 92%

Rule: perf_n_plus_one_queries


🟠 HIGH - Potential undefined response.Parts access (5 occurrences)

Agent: bugs

Category: bug

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/r2.uploader.ts:160 Code accesses response['Parts'] without fallback. S3/R2 ListParts responses may have undefined Parts... Change to: return res.status(200).json(response['Parts'] || []); to match S3Uploader implementatio... 92%
apps/backend/src/api/routes/media.controller.ts:186 Array.pop() returns undefined if Location ends with '/' or has no path separator, even though Locati... Use nullish coalescing: const name = upload.Location.split('/').pop() ?? 'unknown'; 75%
apps/backend/src/api/routes/media.controller.ts:96 Array.pop() on file variable can return undefined if file URL has no separators or is empty, passed ... Use nullish coalescing: const name = file.split('/').pop() ?? 'unknown'; 85%
apps/frontend/src/components/media/new.uploader.tsx:202 Code accesses p.response.body on each element without validation. Uppy responses may have undefined ... Add optional chaining: result.successful.map((p) => p?.response?.body).filter(Boolean) 70%
apps/frontend/src/components/media/new.uploader.tsx:202 Code accesses Object.keys(allRes)[0] which returns undefined if allRes is empty object, then uses un... Check length: const keys = Object.keys(allRes); const items = keys.length > 0 ? (allRes[keys[0]] |... 72%

Rule: bug_null_pointer


🟠 HIGH - File size validation inconsistent between frontend and backend (2 occurrences)

Agent: consistency

Category: quality

📍 View all locations
File Description Suggestion Confidence
apps/frontend/src/components/media/new.uploader.tsx:140-167 Frontend allows 30MB images (line 140) but backend only accepts 10MB (custom.upload.validation.ts li... Align limits: either increase backend to 30MB or reduce frontend to 10MB. Extract to shared constant... 95%
libraries/nestjs-libraries/src/upload/s3.uploader.ts:235-239 S3Uploader validates partNumber with isNaN check and radix 10, but r2.uploader.ts parses without rad... Update r2.uploader.ts line 201 to match: const partNumber = parseInt(req.body.partNumber, 10); if (i... 82%

Rule: cons_reimplemented_validation


🟠 HIGH - Unbounded Promise.all with user-controlled file count

Agent: performance

Category: performance

File: apps/frontend/src/components/media/new.uploader.tsx:202

Description: Promise.all processes all file uploads in parallel without concurrency limits. Users can upload many files causing resource exhaustion.

Suggestion: Add concurrency limiting using p-limit or implement batching. Limit to 5-10 concurrent requests: import pLimit from 'p-limit'; const limit = pLimit(5); await Promise.all(toSave.map(name => limit(() => fetch(...))));

Confidence: 82%

Rule: node_unbounded_promise_all


🟠 HIGH - Path traversal vulnerability in removeFile

Agent: security

Category: security

File: libraries/nestjs-libraries/src/upload/local.storage.ts:77-117

Description: The removeFile method at line 107 accepts arbitrary paths in the catch block fallback. If URL parsing fails, the raw filePath is passed directly to unlink() without validation, allowing potential path traversal attacks.

Suggestion: Validate that resolved path is within uploadDirectory using path.resolve() and startsWith() check before calling unlink. Never accept arbitrary paths without validation.

Confidence: 85%

Rule: sec_path_traversal


🟠 HIGH - Unhandled async operations in event listener (3 occurrences)

Agent: react

Category: bug

📍 View all locations
File Description Suggestion Confidence
apps/frontend/src/components/media/new.uploader.tsx:202 The 'complete' event listener (line 205) is an async callback with Promise.all (line 221) and fetch ... Wrap the async logic in try-catch: `uppy2.on('complete', async (result) => { try { ... } catch (erro... 90%
libraries/react-shared-libraries/src/helpers/uppy.upload.ts:5-19 The fetchUploadApiEndpoint function calls fetch without try-catch and doesn't check response.ok befo... Add error handling: const res = await fetch(...); if (!res.ok) throw new Error(HTTP ${res.status}`... 90%
libraries/react-shared-libraries/src/helpers/uppy.upload.ts:47 The createMultipartUpload callback (line 50) wraps hash calculation in try-catch (lines 56-65) but t... Wrap the entire function body in try-catch or at minimum wrap the fetchUploadApiEndpoint call. 85%

Rule: ts_handle_async_operations_with_proper_erro


🟠 HIGH - Public read ACL hardcoded for all S3 uploads (4 occurrences)

Agent: security

Category: security

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/s3.uploader.ts:109-131 All S3 uploads use hardcoded ACL: 'public-read' (lines 109 and 131). Every uploaded file is publicly... Make ACL configurable based on file type or use case. Consider 'private' as default with signed URLs... 75%
libraries/nestjs-libraries/src/upload/s3.storage.ts:87-105 S3Storage uses hardcoded ACL: 'public-read' in both uploadSimple (line 87) and uploadFile (line 105)... Make ACL configurable. Use 'private' ACL as default and implement signed URLs for controlled access. 75%
libraries/nestjs-libraries/src/upload/cloudflare.storage.ts:95 Cloudflare R2 uploads use hardcoded ACL: 'public-read' (line 95). All uploaded files are publicly ac... Make ACL configurable. Consider 'private' as default with signed URLs for public sharing. 75%
libraries/nestjs-libraries/src/upload/s3.storage.ts:131-159 The removeFile method accepts URLs and extracts S3 keys. While it validates the URL structure (lines... Implement ownership checks at the service layer to verify file path matches expected org/user prefix... 70%

Rule: sec_access_control


🟡 MEDIUM - Duplicated S3Client initialization logic (2 occurrences)

Agent: react

Category: quality

Why this matters: Reduces maintenance cost and prevents logical drift between implementations.

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31 S3Client initialization with Cloudflare credentials is duplicated between r2.uploader.ts and cloudfl... Create a shared function: export function createCloudflareS3Client(): S3Client in a utils file to av... 75%
libraries/nestjs-libraries/src/upload/local.storage.ts:19-20 Local storage duplicates the same yyyy/mm/dd date path generation logic that exists in S3Uploader cl... Extract to a shared utility function like getDatePath() that can be reused across all storage implem... 80%

Rule: ts_extract_duplicated_logic_into_functions


🟡 MEDIUM - Monolithic file with mixed responsibilities (4 occurrences)

Agent: architecture

Category: quality

Why this matters: Functional code with shared state is harder to test and maintain than class-based encapsulation.

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/r2.uploader.ts:38-217 File exports a default handler function plus multiple standalone functions sharing module-level R2 c... Create an R2Uploader class similar to S3Uploader that encapsulates all operations and the client ins... 70%
libraries/nestjs-libraries/src/upload/s3.uploader.ts:76-255 The S3Uploader class mixes HTTP request/response handling (handleUpload accepts Request/Response) wi... Separate into S3StorageClient (S3 operations returning data) and S3UploadHandler (HTTP handling and ... 65%
apps/backend/src/api/routes/media.controller.ts:161-187 The uploadFile method handles provider detection, request routing, response handling, file metadata ... Extract upload orchestration into an UploadOrchestrationService. Controller should delegate and only... 70%
libraries/nestjs-libraries/src/upload/cloudflare.storage.ts:14-66 CloudflareStorage constructor contains complex AWS SDK middleware manipulation (lines 35-61) to remo... Extract middleware configuration into a separate utility function to simplify the constructor. 60%

Rule: arch_srp_violation


🟡 MEDIUM - Cloudflare credentials from env without rotation mechanism (2 occurrences)

Agent: compliance

Category: security

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31 R2 uploader loads Cloudflare credentials directly from process.env without secrets manager integrati... Consider using secrets manager for credential storage and implement credential rotation mechanism if... 65%
libraries/nestjs-libraries/src/upload/s3.uploader.ts:259-276 createS3Uploader() loads AWS credentials from process.env. This is a common pattern but lacks secret... Consider AWS Secrets Manager integration if compliance requirements mandate credential rotation. 60%

Rule: soc2_secrets_manager_rotation


🟡 MEDIUM - Hardcoded user-facing error messages

Agent: accessibility

Category: accessibility

File: apps/frontend/src/components/media/new.uploader.tsx:119-120

Description: Error messages use hardcoded strings instead of the translation system. The useT() hook is imported (line 12) and used elsewhere (line 297) but not for these error messages.

Suggestion: Use the useT() hook for error messages: toast.show(t('upload', 'filetype-not-allowed', { fileType, allowedTypes }))

Confidence: 90%

Rule: fe_hardcoded_strings


🟡 MEDIUM - S3 uploads lack explicit server-side encryption

Agent: compliance

Category: security

File: libraries/nestjs-libraries/src/upload/s3.uploader.ts:104-114

Description: PutObjectCommand does not include ServerSideEncryption field. Files rely on bucket-level default encryption settings if any. Explicit encryption ensures compliance regardless of bucket config.

Suggestion: Add ServerSideEncryption: 'AES256' or 'aws:kms' to PutObjectCommand parameters.

Confidence: 75%

Rule: soc2_encrypt_data_at_rest


🟡 MEDIUM - No audit logging for media deletion (2 occurrences)

Agent: compliance

Category: security

📍 View all locations
File Description Suggestion Confidence
apps/backend/src/api/routes/media.controller.ts:50-51 The deleteMedia endpoint directly calls _mediaService.deleteMedia() without audit logging. No record... Add audit logging before deletion: log actor (user/org ID), action, resource ID, timestamp. 80%
apps/backend/src/api/routes/media.controller.ts:161-187 The uploadFile endpoint handling multipart upload completion does not log audit information for comp... Implement audit logging with: user, org, filename, file hash, completion status, timestamp for COMPL... 70%

Rule: compliance_audit_trail


🟡 MEDIUM - Raw error objects logged to console

Agent: compliance

Category: security

File: libraries/nestjs-libraries/src/upload/s3.uploader.ts:143-145

Description: Error objects from S3 API calls are logged directly without filtering. Error objects may contain sensitive information like request IDs or bucket names.

Suggestion: Implement structured logging with error sanitization: console.error('S3 error', { message: err.message, code: err.code })

Confidence: 75%

Rule: soc2_mask_pii_in_logs


🟡 MEDIUM - Duplicate getDatePath method (3 occurrences)

Agent: consistency

Category: quality

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/upload/s3.storage.ts:63-69 getDatePath() is implemented identically in S3Storage (lines 63-69) and S3Uploader (lines 68-74). Sh... Extract to s3.utils.ts: export function getDatePath(): string { ... } 90%
libraries/nestjs-libraries/src/upload/s3.uploader.ts:64-66 generateRandomString() simply returns makeId(20). This adds unnecessary indirection when makeId is a... Remove generateRandomString() and use makeId(20) directly in the calling code. 85%
libraries/nestjs-libraries/src/upload/upload.factory.ts:6-13 validateEnvVars function duplicates similar logic in S3Uploader.createS3Uploader. Could be consolida... Export validateEnvVars from upload.factory.ts and reuse in S3Uploader.createS3Uploader, or create a ... 65%

Rule: cons_duplicate_utility_function


🟡 MEDIUM - Debug console.log in production code

Agent: quality

Category: quality

File: apps/backend/src/api/routes/media.controller.ts:51

Description: console.log('hello') appears to be leftover debug code with no diagnostic value.

Suggestion: Remove this debugging statement entirely.

Confidence: 100%

Rule: quality_avoid_console_in_production


🟡 MEDIUM - No authorization check in updateIntegration method (3 occurrences)

Agent: compliance

Category: security

📍 View all locations
File Description Suggestion Confidence
libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:146-150 updateIntegration method updates integration by ID only, without verifying organization ownership or... Add organization check: where: { id, organizationId } or verify integration belongs to requesting or... 85%
apps/backend/src/api/routes/media.controller.ts:50-120 Media endpoints verify organization via @GetOrgFromRequest() but deleteMedia doesn't verify the medi... In MediaService.deleteMedia, verify media.organizationId === org before allowing deletion. Example: ... 75%
libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:173-294 createOrUpdateIntegration upserts sensitive token data. While org ID is checked via composite key, n... Consider adding role check for token modifications and audit logging for all token changes. 65%

Rule: soc2_rbac_least_privilege


🟡 MEDIUM - Duplicate S3 configuration interface

Agent: consistency

Category: quality

File: libraries/nestjs-libraries/src/upload/s3.storage.ts:14-21

Description: S3StorageConfig and S3UploaderConfig have identical fields. s3.utils.ts already defines a shared S3Config interface that could be extended.

Suggestion: Have S3StorageConfig and S3UploaderConfig extend the S3Config interface from s3.utils.ts, adding only accessKey/secretKey.

Confidence: 80%

Rule: cons_duplicate_type_definition


🟡 MEDIUM - Hardcoded file size limits should be configurable (2 occurrences)

Agent: general

Category: style

📍 View all locations
File Description Suggestion Confidence
apps/frontend/src/components/media/new.uploader.tsx:140-141 Image and video size limits are hardcoded. These should be environment-configurable for different de... Move limits to environment variables passed via VariableContext: MAX_IMAGE_SIZE_MB and MAX_VIDEO_SIZ... 70%
apps/frontend/src/components/media/new.uploader.tsx:192-196 Compression settings (maxWidth: 1000, maxHeight: 1000, quality: 1) are hardcoded, limiting deploymen... Move compression settings to environment variables or configuration context for different deployment... 60%

Rule: general_hardcoded_config


🟡 MEDIUM - Unsafe type casting for response object

Agent: react

Category: quality

File: apps/backend/src/api/routes/media.controller.ts:181-187

Description: Multiple @ts-ignore comments (lines 181, 185, 191) bypass TypeScript checks when accessing upload.Location. This indicates improper typing of the CompleteMultipartUploadCommandOutput response.

Suggestion: Define proper types for the upload response and use type guards: if (upload && 'Location' in upload) { ... }

Confidence: 80%

Rule: ts_classify_http_errors_with_type_safe_narr


🟡 MEDIUM - Missing dependency in useCallback

Agent: react

Category: quality

File: apps/frontend/src/components/media/new.uploader.tsx:29-35

Description: The useCallback at line 29 has [onUploadSuccess] in dependencies but the original claim was empty array. Verified: line 34 shows [onUploadSuccess] - this issue appears to have been fixed.

Suggestion: No action needed - dependency array is correct.

Confidence: 20%

Rule: react_inline_function_props


🟡 MEDIUM - Missing validation in multipart upload context

Agent: security

Category: security

File: apps/backend/src/api/routes/media.controller.ts:161-187

Description: The uploadFile endpoint routes multipart upload requests (line 157-170) based on endpoint parameter. While org is retrieved via @GetOrgFromRequest(), there's no validation that the uploadId/key belongs to the authenticated organization.

Suggestion: Validate that multipart upload context (uploadId, key) belongs to the authenticated organization before processing.

Confidence: 70%

Rule: sec_broken_access_control


🔵 LOW - uppy.log() calls without dev-only checks

Agent: accessibility

Category: quality

File: apps/frontend/src/components/media/new.uploader.tsx:115-165

Description: Multiple uppy2.log() and uppy2.info() calls throughout the preprocessor code. These are Uppy's built-in logging methods which may be acceptable, but could be wrapped in development checks.

Suggestion: Wrap in development check if logging should be suppressed in production: if (process.env.NODE_ENV === 'development') { uppy2.log(...); }

Confidence: 65%

Rule: fe_console_in_production


🔵 LOW - Magic number 32 in filename generation

Agent: quality

Category: quality

File: libraries/nestjs-libraries/src/upload/local.storage.ts:20

Description: The number 32 is used without explanation for generating random filenames.

Suggestion: Extract to named constant: private readonly RANDOM_FILENAME_LENGTH = 32;

Confidence: 70%

Rule: qual_magic_numbers_js


🔵 LOW - Global window assignment with @ts-ignore

Agent: react

Category: security

File: libraries/react-shared-libraries/src/helpers/variable.context.tsx:58-61

Description: The code assigns application configuration to window.vars (line 60) using @ts-ignore (line 59). This exposes environment variables and configuration globally, which could leak sensitive information to browser console.

Suggestion: Review what data is exposed via otherProps and ensure no sensitive information is included. Consider using a more controlled state access pattern.

Confidence: 60%

Rule: ts_avoid_unsafe_type_assertions


ℹ️ 19 issue(s) outside PR diff (click to expand)

These issues were found in lines not modified in this PR.

🔴 CRITICAL - Unhandled promise rejection in fetch().json()

Agent: bugs

Category: bug

Why this matters: Unhandled rejections in Promise.all will fail silently or crash the operation without proper user feedback.

File: apps/frontend/src/components/media/new.uploader.tsx:224-230

Description: fetch() and .json() calls inside Promise.all have no error handling. Network failures or invalid JSON will cause unhandled promise rejection.

Suggestion: Wrap in try-catch: try { const res = await fetch(...); if (!res.ok) throw new Error(...); return res.json(); } catch (error) { ... }

Confidence: 85%

Rule: bug_unhandled_promise


🟠 HIGH - Module-level S3 client reads env vars at import time

Agent: architecture

Category: quality

File: libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31

Description: The R2 client is instantiated at module level, reading process.env variables during import. Missing env vars cause module load failures. Also makes testing difficult.

Suggestion: Move client creation into a factory function or service class with lazy initialization and dependency injection support.

Confidence: 85%

Rule: arch_singleton_state_js


🟠 HIGH - Potential undefined from split().pop()

Agent: bugs

Category: bug

File: apps/backend/src/api/routes/media.controller.ts:96

Description: Array.pop() on file variable can return undefined if file URL has no separators or is empty, passed directly to saveFile.

Suggestion: Use nullish coalescing: const name = file.split('/').pop() ?? 'unknown';

Confidence: 85%

Rule: bug_null_pointer


🟠 HIGH - File size validation inconsistent between frontend and backend

Agent: consistency

Category: quality

File: apps/frontend/src/components/media/new.uploader.tsx:140-167

Description: Frontend allows 30MB images (line 140) but backend only accepts 10MB (custom.upload.validation.ts line 39). Users will see confusing rejection errors.

Suggestion: Align limits: either increase backend to 30MB or reduce frontend to 10MB. Extract to shared constants file.

Confidence: 95%

Rule: cons_reimplemented_validation


🟠 HIGH - Sequential database updates in loop - N+1 pattern

Agent: performance

Category: performance

File: libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:529-538

Description: disableIntegrations updates channels one-by-one in a loop. For N channels, this executes N individual UPDATE queries.

Suggestion: Use updateMany: await this._integration.model.integration.updateMany({ where: { id: { in: getChannels.map(c => c.id) } }, data: { disabled: true } });

Confidence: 92%

Rule: perf_n_plus_one_queries


🟠 HIGH - Unhandled promise rejection in fetch call

Agent: react

Category: bug

File: libraries/react-shared-libraries/src/helpers/uppy.upload.ts:5-19

Description: The fetchUploadApiEndpoint function calls fetch without try-catch and doesn't check response.ok before calling .json(). Network errors and error HTTP responses will cause unhandled rejections.

Suggestion: Add error handling: const res = await fetch(...); if (!res.ok) throw new Error(HTTP ${res.status}); return res.json(); Wrap in try-catch.

Confidence: 90%

Rule: ts_handle_async_operations_with_proper_erro


🟠 HIGH - Public ACL hardcoded for Cloudflare R2 uploads

Agent: security

Category: security

File: libraries/nestjs-libraries/src/upload/cloudflare.storage.ts:95

Description: Cloudflare R2 uploads use hardcoded ACL: 'public-read' (line 95). All uploaded files are publicly accessible.

Suggestion: Make ACL configurable. Consider 'private' as default with signed URLs for public sharing.

Confidence: 75%

Rule: sec_access_control


🟡 MEDIUM - Duplicated S3Client initialization logic

Agent: react

Category: quality

Why this matters: Reduces maintenance cost and prevents logical drift between implementations.

File: libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31

Description: S3Client initialization with Cloudflare credentials is duplicated between r2.uploader.ts and cloudflare.storage.ts with similar endpoint and credential setup.

Suggestion: Create a shared function: export function createCloudflareS3Client(): S3Client in a utils file to avoid duplication.

Confidence: 75%

Rule: ts_extract_duplicated_logic_into_functions


🟡 MEDIUM - Monolithic file with mixed responsibilities

Agent: architecture

Category: quality

Why this matters: Functional code with shared state is harder to test and maintain than class-based encapsulation.

File: libraries/nestjs-libraries/src/upload/r2.uploader.ts:38-217

Description: File exports a default handler function plus multiple standalone functions sharing module-level R2 client singleton, mixing concerns of HTTP handling and S3 operations.

Suggestion: Create an R2Uploader class similar to S3Uploader that encapsulates all operations and the client instance.

Confidence: 70%

Rule: arch_srp_violation


🟡 MEDIUM - Cloudflare credentials from env without rotation mechanism

Agent: compliance

Category: security

File: libraries/nestjs-libraries/src/upload/r2.uploader.ts:16-31

Description: R2 uploader loads Cloudflare credentials directly from process.env without secrets manager integration. No credential rotation mechanism is implemented.

Suggestion: Consider using secrets manager for credential storage and implement credential rotation mechanism if compliance requirements demand it.

Confidence: 65%

Rule: soc2_secrets_manager_rotation


🟡 MEDIUM - Hardcoded user-facing error messages

Agent: accessibility

Category: accessibility

File: apps/frontend/src/components/media/new.uploader.tsx:119-120

Description: Error messages use hardcoded strings instead of the translation system. The useT() hook is imported (line 12) and used elsewhere (line 297) but not for these error messages.

Suggestion: Use the useT() hook for error messages: toast.show(t('upload', 'filetype-not-allowed', { fileType, allowedTypes }))

Confidence: 90%

Rule: fe_hardcoded_strings


🟡 MEDIUM - useEffect missing dependency - stale window.vars (2 occurrences)

Agent: bugs

Category: bug

📍 View all locations
File Description Suggestion Confidence
libraries/react-shared-libraries/src/helpers/variable.context.tsx:57-62 The useEffect has an empty dependency array [] but uses otherProps. If props change after mount, win... Add otherProps to the dependency array: useEffect(() => { ... }, [otherProps]) or use individual pro... 85%
libraries/nestjs-libraries/src/upload/r2.uploader.ts:199-217 The signPart function parses partNumber using parseInt() without checking for NaN. Unlike s3.uploade... Add validation: if (isNaN(partNumber)) { return res.status(400).json({ error: 'Invalid part number' ... 80%

Rule: gen_double_charge_no_optimistic_lock


🟡 MEDIUM - Missing accessibility attributes on Dashboard

Agent: accessibility

Category: accessibility

Why this matters: File upload interfaces should be accessible to screen reader users. However, Uppy Dashboard may have its own accessibility features.

File: apps/frontend/src/components/media/new.uploader.tsx:278-292

Description: The Uppy Dashboard component wrapper div has class 'pointer-events-none' but lacks ARIA labels for screen reader users.

Suggestion: Add aria-label to the wrapping div:

Confidence: 65%

Rule: fe_aria_live_regions


🟡 MEDIUM - Token updates without role-based authorization

Agent: compliance

Category: security

File: libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts:173-294

Description: createOrUpdateIntegration upserts sensitive token data. While org ID is checked via composite key, no role verification for token operations.

Suggestion: Consider adding role check for token modifications and audit logging for all token changes.

Confidence: 65%

Rule: soc2_rbac_least_privilege


🟡 MEDIUM - Hardcoded file size limits should be configurable

Agent: general

Category: style

File: apps/frontend/src/components/media/new.uploader.tsx:140-141

Description: Image and video size limits are hardcoded. These should be environment-configurable for different deployment scenarios.

Suggestion: Move limits to environment variables passed via VariableContext: MAX_IMAGE_SIZE_MB and MAX_VIDEO_SIZE_MB with defaults.

Confidence: 70%

Rule: general_hardcoded_config


🟡 MEDIUM - Missing dependency in useCallback

Agent: react

Category: quality

File: apps/frontend/src/components/media/new.uploader.tsx:29-35

Description: The useCallback at line 29 has [onUploadSuccess] in dependencies but the original claim was empty array. Verified: line 34 shows [onUploadSuccess] - this issue appears to have been fixed.

Suggestion: No action needed - dependency array is correct.

Confidence: 20%

Rule: react_inline_function_props


🔵 LOW - uppy.log() calls without dev-only checks

Agent: accessibility

Category: quality

File: apps/frontend/src/components/media/new.uploader.tsx:115-165

Description: Multiple uppy2.log() and uppy2.info() calls throughout the preprocessor code. These are Uppy's built-in logging methods which may be acceptable, but could be wrapped in development checks.

Suggestion: Wrap in development check if logging should be suppressed in production: if (process.env.NODE_ENV === 'development') { uppy2.log(...); }

Confidence: 65%

Rule: fe_console_in_production


🔵 LOW - Global window assignment with @ts-ignore

Agent: react

Category: security

File: libraries/react-shared-libraries/src/helpers/variable.context.tsx:58-61

Description: The code assigns application configuration to window.vars (line 60) using @ts-ignore (line 59). This exposes environment variables and configuration globally, which could leak sensitive information to browser console.

Suggestion: Review what data is exposed via otherProps and ensure no sensitive information is included. Consider using a more controlled state access pattern.

Confidence: 60%

Rule: ts_avoid_unsafe_type_assertions



Review ID: bf12b8d4-b098-463e-a323-e769519b003d
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@raymatos
Copy link

Any update on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants